/*
 * MatrixMarketReader.cs
 *
 * Creates matrices from Matrix Market files.
 *
 * Copyright (c) 2005, dnAnalytics. All rights reserved.
 */

#region Using Directives
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using dnAnalytics.Resources;
using dnAnalytics.LinearAlgebra;
#endregion Using Directives

namespace dnAnalytics.LinearAlgebra.IO
{
    /// <summary>
    /// Creates matrices from Matrix Market files.
    /// </summary>
    /// <remarks>see <a href="http://math.nist.gov/MatrixMarket/formats.html">http://math.nist.gov/MatrixMarket/formats.html</a> for more information.</remarks>
    /// <include file='../../../../examples/trunk/LinearAlgebra/IO/MatrixMarketReaderOverall.xml' path='example'/> 
    public class MatrixMarketReader : IMatrixReader
    {
        #region Fields

        private static Regex spaces = new Regex(" +");

        #endregion Fields

        #region Public Methods

        /// <summary>
        /// Reads a <see cref="Matrix"/> from a file.
        /// </summary>
        /// <param name="file">The file to read the matrix from.</param>
        /// <returns>A matrix containing the data from the file. <b>null</b> is returned if the file is empty.</returns> 
        /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <b>null</b>.</exception>
        /// <exception cref="IOException">If the file doesn't exist.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(string file)
        {
            if (file == null)
            {
                throw new ArgumentNullException("file", Strings.NullParameterException);
            }

            if (!File.Exists(file))
            {
                throw new IOException(Strings.FileDoesNotExist);
            }
            return ReadMatrix(File.OpenRead(file), Settings.DefaultMatrixType);
        }

        /// <summary>
        /// Reads a <see cref="Matrix"/> from a <see cref="Stream"/>.
        /// </summary>
        /// <param name="stream">The <see cref="Stream"/> to read the matrix from.</param>
        /// <returns>A matrix containing the data from the <see cref="Stream"/>. <b>null</b> is returned if the <see cref="Stream"/> is empty.</returns>        
        /// <exception cref="ArgumentNullException">If <paramref name="stream"/> is <b>null</b>.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(Stream stream)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream", Strings.NullParameterException);
            }

            using (StreamReader reader = new StreamReader(new BufferedStream(stream)))
            {
                return (Matrix)ReadMatrix(reader, Settings.DefaultMatrixType);
            }
        }

        /// <summary>
        /// Reads a <see cref="Matrix"/> from a <see cref="TextReader"/>.
        /// </summary>
        /// <param name="reader">The <see cref="TextReader"/> to read the matrix from.</param>
        /// <returns>A matrix containing the data from the <see cref="TextReader"/>. <b>null</b> is returned if the <see cref="TextReader"/> is empty.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="reader"/> is <b>null</b>.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(TextReader reader)
        {
            if (reader == null)
            {
                throw new ArgumentNullException("reader", Strings.NullParameterException);
            }
            return ReadMatrix(reader, Settings.DefaultMatrixType);
        }

        /// <summary>
        /// Reads an <see cref="Matrix"/> from a file.
        /// </summary>
        /// <param name="file">The file to read the matrix from.</param>
        /// <param name="matrixType">The type of matrix to return. The <see cref="Type"/> must implement <see cref="Matrix"/>.</param>
        /// <returns>A matrix containing the data from the file. <b>null</b> is returned if the file is empty.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <b>null</b>.</exception>
        /// <exception cref="IOException">If the file doesn't exist.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(string file, MatrixType matrixType)
        {
            if (file == null)
            {
                throw new ArgumentNullException("file", Strings.NullParameterException);
            }

            if (!File.Exists(file))
            {
                throw new IOException(Strings.FileDoesNotExist);
            }
            return ReadMatrix(File.OpenRead(file), matrixType);
        }

        /// <summary>
        /// Reads an <see cref="Matrix"/> from a <see cref="Stream"/>.
        /// </summary>
        /// <param name="stream">The <see cref="Stream"/> to read the matrix from.</param>
        /// <param name="matrixType">The type of matrix to return. The <see cref="Type"/> must implement <see cref="Matrix"/>.</param>
        /// <returns>A matrix containing the data from the <see cref="Stream"/>. <b>null</b> is returned if the <see cref="Stream"/> is empty.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="stream"/> is <b>null</b>.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(Stream stream, MatrixType matrixType)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream", Strings.NullParameterException);
            }
            using (StreamReader reader = new StreamReader(new BufferedStream(stream)))
            {
                return ReadMatrix(reader, matrixType);
            }
        }

        /// <summary>
        /// Reads an <see cref="Matrix"/> from a <see cref="TextReader"/>.
        /// </summary>
        /// <param name="reader">The <see cref="TextReader"/> to read the matrix from.</param>
        /// <param name="matrixType">The type of matrix to return. The <see cref="Type"/> must implement <see cref="Matrix"/>.</param>
        /// <returns>A matrix containing the data from the <see cref="TextReader"/>. <b>null</b> is returned if the <see cref="TextReader"/> is empty.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="reader"/>.</exception>
        /// <exception cref="FormatException">If a value is not a number or not in a valid format.</exception>
        /// <exception cref="OverflowException">If a value represents a number less than <see cref="Double.MinValue"/> or greater than <see cref="Double.MaxValue"/>.</exception>
        public Matrix ReadMatrix(TextReader reader, MatrixType matrixType)
        {
            if (reader == null)
            {
                throw new ArgumentNullException("reader", Strings.NullParameterException);
            }


            string line = reader.ReadLine().ToLower(CultureInfo.InvariantCulture);
            if (line == null || line.Length == 0)
            {
                return null;
            }

            string[] args = spaces.Split(line);
            if (!args[0].Equals("%%matrixmarket"))
            {
                throw new FormatException(Strings.NotMatrixMarketFile);
            }
            if (args.Length < 5)
            {
                throw new FormatException(Strings.NotProperHeader);
            }

            if (!args[1].Equals("matrix"))
            {
                throw new FormatException(Strings.NotMatrixMarketFile);
            }

            bool isArray;
            if (args[2].Equals("array"))
            {
                isArray = true;
            }
            else if (args[2].Equals("coordinate"))
            {
                isArray = false;
            }
            else
            {
                throw new FormatException(Strings.NotProperlyFormattedMatrixMarketFile);
            }

            DataType type = DataType.Real;
            if (args[3].Equals("complex"))
            {
                throw new NotSupportedException(Strings.ComplexNotSupported);
            }
            if (args[3].Equals("pattern"))
            {
                type = DataType.Pattern;
            }

            StorageFormat format;
            if (args[4].Equals("general"))
            {
                format = StorageFormat.General;
            }
            else if (args[4].Equals("symmetric"))
            {
                format = StorageFormat.Symmetric;
            }
            else if (args[4].Equals("skew-symmetric"))
            {
                format = StorageFormat.SkewSymmetric;
            }
            else
            {
                throw new FormatException(Strings.NotProperlyFormattedMatrixMarketFile);
            }

            if (type == DataType.Pattern && format == StorageFormat.SkewSymmetric)
            {
                throw new FormatException(Strings.NotProperlyFormattedMatrixMarketFile);
            }

            //get matrix dimensions
            line = reader.ReadLine();

            //skip comments
            while (line.StartsWith("%") || line.Length == 0)
            {
                line = reader.ReadLine();
            }
            args = spaces.Split(line.Trim());
            if (args.Length < 2)
            {
                throw new FormatException(Strings.NotProperlyFormattedMatrixMarketFile);
            }

            int rows = Int32.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture);
            int cols = Int32.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture);

            Matrix ret = MatrixBuilder.CreateMatrix(rows, cols, matrixType);
            if (isArray)
            {
                if (type == DataType.Pattern)
                {
                    throw new FormatException(Strings.NotProperlyFormattedMatrixMarketFile);
                }
                ArrayReader(ret, reader, format, rows);
            }
            else
            {
                CoordinateReader(ret, reader, format, type);
            }
            return ret;
        }
        #endregion Public Methods

        #region Private Methods


        /// <summary>
        /// Reads in a matrix market file using an array layout.
        /// </summary>
        /// <param name="ret">The matrix to fill.</param>
        /// <param name="reader">The TextReader to read from.</param>
        /// <param name="format">The storage format to use.</param>
        /// <param name="rows">The number of rows.</param>
        private static void ArrayReader(Matrix ret, TextReader reader, StorageFormat format, int rows)
        {
            string line = reader.ReadLine();

            int row = 0;
            int col = 0;
            if (format == StorageFormat.SkewSymmetric)
            {
                row = 1;
            }
            while (line != null)
            {
                if (line.Length > 0)
                {
                    double value = Double.Parse(line.Trim(), NumberStyles.Any, null);
                    ret[row, col] = value;
                    if (format == StorageFormat.Symmetric)
                    {
                        ret[col, row] = value;
                    }
                    else if (format == StorageFormat.SkewSymmetric)
                    {
                        ret[col, row] = -value;
                    }

                    row++;
                    //reached end of column
                    if (row == rows)
                    {
                        row = 0;
                        col++;
                        if (format == StorageFormat.Symmetric)
                        {
                            row = col;
                        }
                        else if (format == StorageFormat.SkewSymmetric)
                        {
                            row = col + 1;
                        }
                    }
                }
                line = reader.ReadLine();
            }
        }

        /// <summary>
        /// Reads in a matrix market file using a coordinate layout.
        /// </summary>
        /// <param name="ret">The matrix to fill.</param>
        /// <param name="reader">The TextReader to use.</param>
        /// <param name="format">The storage format.</param>
        /// <param name="type">The data type.</param>
        private static void CoordinateReader(Matrix ret, TextReader reader, StorageFormat format, DataType type)
        {
            string line = reader.ReadLine();
            while (line != null)
            {
                if (line.Length > 0)
                {
                    string[] values = spaces.Split(line.Trim());
                    int row = Int32.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) - 1;
                    int col = Int32.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture) - 1;

                    double value = 1;
                    if (type != DataType.Pattern)
                    {
                        value = Double.Parse(values[2], NumberStyles.Any, null);
                    }
                    ret[row, col] = value;
                    if (format == StorageFormat.Symmetric)
                    {
                        ret[col, row] = value;
                    }
                }
                line = reader.ReadLine();
            }
        }

        #endregion Private Methods
    }
}